Необходимо проанализировать поведение пользователей мобильного приложения для продажи продуктов питания.
Следует изучить воронку продаж и проанализировать результаты A/A/B-эксперимента. В ходе эксперимента пользователи были разделены на три группы: две контрольные группы использовали старые шрифты, а одна экспериментальная группа — новые шрифты. Целью эксперимента было выяснить, какой шрифт больше нравится пользователям.
Таблица logs_exp.csv. Каждая запись в логе — это действие пользователя, или событие.
#импорт библиотек
import pandas as pd
import numpy as np
import math as mth
import matplotlib.pyplot as plt
import scipy.stats as st
import datetime as dt
from IPython.display import display
from plotly import graph_objects as go
#Чтение датасета
try:
data = pd.read_csv('datasets/logs_exp.csv', sep='\t')
except:
data = pd.read_csv('https://code.s3.yandex.net/datasets/logs_exp.csv', sep='\t')
#Создаем функцию для изучения датасета, преведения названий строк к змеиному регистру и подсчету полных дубликатов
def data_info(data):
data.columns = [(x.lower()).replace(' ', '_') for x in data.columns]
print('-' * 80)
display(data.head())
print('-' * 80)
data.info()
print('-' * 80)
print('Кол-во полных дубликатов в таблице:', data.duplicated().sum())
print('-' * 80)
#Создадим функцию для просмотра максимальной и минимальной дате в столбцах
def data_dt_info(data, dt):
print('Минимальная дата в столбце', dt, data[dt].min(),
'\nМаксимальная дата в столбце', dt, data[dt].max())
#Чтение датасета
data_info(data)
--------------------------------------------------------------------------------
| eventname | deviceidhash | eventtimestamp | expid | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 |
| 3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 |
-------------------------------------------------------------------------------- <class 'pandas.core.frame.DataFrame'> RangeIndex: 244126 entries, 0 to 244125 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 eventname 244126 non-null object 1 deviceidhash 244126 non-null int64 2 eventtimestamp 244126 non-null int64 3 expid 244126 non-null int64 dtypes: int64(3), object(1) memory usage: 7.5+ MB -------------------------------------------------------------------------------- Кол-во полных дубликатов в таблице: 413 --------------------------------------------------------------------------------
#Изменим название колонок
data.columns = ['event_name', 'user_id', 'dt', 'group']
#Переведем дату в удобный формат для чтения и работы
data.dt = data.dt.map(lambda x: dt.datetime.fromtimestamp(x))
#Выведим первые строки таблицы
display(data.head())
#Посмотрим минимальную и максимальную дату
data_dt_info(data, 'dt')
| event_name | user_id | dt | group | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 2019-07-25 07:43:36 | 246 |
| 1 | MainScreenAppear | 7416695313311560658 | 2019-07-25 14:11:42 | 246 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 2019-07-25 14:28:47 | 248 |
| 3 | CartScreenAppear | 3518123091307005509 | 2019-07-25 14:28:47 | 248 |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 2019-07-25 14:48:42 | 248 |
Минимальная дата в столбце dt 2019-07-25 07:43:36 Максимальная дата в столбце dt 2019-08-08 00:15:17
#Создаю переменные, где будет хранится первоначально число строк и уникальных пользователей
count_entries = data.shape[0]
count_user = data.user_id.nunique()
#Создаем функцию, которая будет выводить, сколько было удаленно строк и уникальных пользователей
def difference_of_values():
print(f'Всего удаленно строк {count_entries - data.shape[0]} \
или {round(100 * (count_entries - data.shape[0]) / count_entries, 2)}% от первоначального кол-во строк')
print(f'Всего удаленно уникальных пользователей {count_user - data.user_id.nunique()} \
или {round(100 * (count_user - data.user_id.nunique()) / count_user, 2)}% от первоначального \
кол-ва уникальных пользователей')
#Удалим полные дубликаты из таблицы
data = data.drop_duplicates()
#Посмотрим, сколько данных удалилось
difference_of_values()
Всего удаленно строк 413 или 0.17% от первоначального кол-во строк Всего удаленно уникальных пользователей 0 или 0.0% от первоначального кол-ва уникальных пользователей
#Посмотрим, какие события есть в логах
data.event_name.unique()
array(['MainScreenAppear', 'PaymentScreenSuccessful', 'CartScreenAppear',
'OffersScreenAppear', 'Tutorial'], dtype=object)
Мы видим, что есть 5 видов событий, которые отображаются в логах
#Немного изменим названия событий, для лучшей их читаемости
data.event_name = data.event_name.replace({'MainScreenAppear': 'Main_Screen_Appear',
'PaymentScreenSuccessful': 'Payment_Screen_Successful',
'CartScreenAppear': 'Cart_Screen_Appear',
'OffersScreenAppear': 'Offers_Screen_Appear'})
#Посмотрим, кол-во уникальных пользователей в таблице и проверим, что в таблице только нужные группы пользователей
print('Кол-во уникальных пользователей', data.user_id.nunique(), '\nНомера всех групп', data.group.unique())
Кол-во уникальных пользователей 7551 Номера всех групп [246 248 247]
#Добавляем отдельный столбец с датой, временем и днем эксперимента
data['date'] = pd.to_datetime(data.dt).dt.date
data['time'] = pd.to_datetime(data.dt).dt.time
data['day'] = data.date - min(data.date)
data.head()
| event_name | user_id | dt | group | date | time | day | |
|---|---|---|---|---|---|---|---|
| 0 | Main_Screen_Appear | 4575588528974610257 | 2019-07-25 07:43:36 | 246 | 2019-07-25 | 07:43:36 | 0 days |
| 1 | Main_Screen_Appear | 7416695313311560658 | 2019-07-25 14:11:42 | 246 | 2019-07-25 | 14:11:42 | 0 days |
| 2 | Payment_Screen_Successful | 3518123091307005509 | 2019-07-25 14:28:47 | 248 | 2019-07-25 | 14:28:47 | 0 days |
| 3 | Cart_Screen_Appear | 3518123091307005509 | 2019-07-25 14:28:47 | 248 | 2019-07-25 | 14:28:47 | 0 days |
| 4 | Payment_Screen_Successful | 6217807653094995999 | 2019-07-25 14:48:42 | 248 | 2019-07-25 | 14:48:42 | 0 days |
# Выведим кол-во событий, пользователей и среднее кол-во событий на пользователя
print(f'Кол-во событий в логе: {data.shape[0]}. \
\nУникальных пользователей в логе: {data.user_id.nunique()}. \
\nСреднее кол-во событий на пользователя: {round(data.shape[0] / data.user_id.nunique(), 1)}')
Кол-во событий в логе: 243713. Уникальных пользователей в логе: 7551. Среднее кол-во событий на пользователя: 32.3
# Посмотрим описательную статистику кол-ва событий на пользователя
data.groupby('user_id').dt.count().describe()
count 7551.000000 mean 32.275593 std 65.154219 min 1.000000 25% 9.000000 50% 20.000000 75% 37.000000 max 2307.000000 Name: dt, dtype: float64
По описательной статистике, мы видим, что большинство пользователей за время исследования совершили от 9 до 37 действий. А среднее кол-во событий на пользователя равно 32.3.
# Посмотрим минимальные и максимальные даты по группам
for i in data.group.unique():
print(f'''В группе {i} минимальная дата: {data.query('group == @i').date.min()}, \
максимальная дата: {data.query('group == @i').date.max()}''')
print('-'*75)
В группе 246 минимальная дата: 2019-07-25, максимальная дата: 2019-08-08 --------------------------------------------------------------------------- В группе 248 минимальная дата: 2019-07-25, максимальная дата: 2019-08-08 --------------------------------------------------------------------------- В группе 247 минимальная дата: 2019-07-25, максимальная дата: 2019-08-08 ---------------------------------------------------------------------------
Минимальные и максимальные даты в группах равны: 2019-07-25 и 2019-08-08
# Создаем агрегированную таблицу по группам и датам, и считаем кол-во событий
data_group = data.groupby(['group', 'day'], as_index=False) \
.agg({'day': 'max', 'date': 'max', 'user_id': 'count'}) \
.sort_values(['day', 'group']).reset_index(drop=True)
data_group.head()
| group | day | date | user_id | |
|---|---|---|---|---|
| 0 | 246 | 0 days | 2019-07-25 | 4 |
| 1 | 247 | 0 days | 2019-07-25 | 1 |
| 2 | 248 | 0 days | 2019-07-25 | 4 |
| 3 | 246 | 1 days | 2019-07-26 | 14 |
| 4 | 247 | 1 days | 2019-07-26 | 8 |
#Задаем ширину столбца
barWidth = 0.25
#Задаем высоту столбцов для каждого дня по группам
bars1 = data_group.query('group == 246').user_id
bars2 = data_group.query('group == 247').user_id
bars3 = data_group.query('group == 248').user_id
#Задаем позицию для каждого столбца
r1 = np.arange(len(bars1))
r2 = [x + barWidth for x in r1]
r3 = [x + barWidth for x in r2]
#Строим график
plt.figure(figsize=(14, 6))
plt.bar(r1, bars1, color='#7f6d5f', width=barWidth, edgecolor='white', label='группа 246')
plt.bar(r2, bars2, color='#557f2d', width=barWidth, edgecolor='white', label='группа 247')
plt.bar(r3, bars3, color='#2d7f5e', width=barWidth, edgecolor='white', label='группа 248')
#Настраиваем график
plt.title('Столбчатая диаграмма кол-ва событий по группам', size=14)
plt.xlabel('Дата', size=12)
plt.ylabel('Кол-во событий', size=12)
x = [str(x.day) + '.' + '0' + str(x.month) for x in list(data.date.unique())]
plt.xticks([r + barWidth for r in range(len(bars1))], x)
plt.yticks(range(0, 15000, 1000))
plt.ylim(0, 14000)
plt.legend()
plt.show()
#Также отдельно посмотрим, что происходило до резкого всплекска
data[(data['date'] < dt.datetime(2019, 8, 1).date())].dt.hist(bins=100, figsize=(12,6))
<AxesSubplot:>
По графику видно, что большинство событий происходят с 1.08 по 7.08 включительно. Для дальнейшего анализа будем использовать события в эти дни.
Также можно сказать, что большинство логов приходят во второй половине дня.
#Отбросим события, которые выходят за определенные даты
data = data[(data['date'] >= dt.datetime(2019, 8, 1).date())
& (data['date'] <= dt.datetime(2019, 8, 7).date())].reset_index(drop=True)
#Посмотрим, сколько событий удалили
difference_of_values()
print('-'*105)
#Посмотрим, кол-во уникальный пользователей по группам
print('Кол-во уникальных пользователей по группам')
data.groupby('group').agg({'user_id': 'nunique'})
Всего удаленно строк 2470 или 1.01% от первоначального кол-во строк Всего удаленно уникальных пользователей 13 или 0.17% от первоначального кол-ва уникальных пользователей --------------------------------------------------------------------------------------------------------- Кол-во уникальных пользователей по группам
| user_id | |
|---|---|
| group | |
| 246 | 2484 |
| 247 | 2517 |
| 248 | 2537 |
#Считаем частоту встречи событий в логах
logs_event = data.groupby('event_name').agg({'user_id': 'count'}).sort_values('user_id', ascending=False).reset_index()
logs_event['%'] = round(logs_event.user_id / data.user_id.count() * 100, 2)
#Строим график
plt.figure(figsize=(10, 4))
plt.barh(logs_event.event_name, logs_event.user_id)
#Настраиваем график
plt.title('Диаграмма кол-ва событий', size=16)
plt.xlabel('Кол-во события в логах', size=14)
plt.ylabel('Событие', size=14)
plt.xticks(range(0, 130000, 15000), size=12)
plt.yticks(size=12)
plt.xlim(0, 120000)
plt.grid(axis='x', color='grey', alpha=0.5)
plt.show()
logs_event
| event_name | user_id | % | |
|---|---|---|---|
| 0 | Main_Screen_Appear | 117850 | 48.77 |
| 1 | Offers_Screen_Appear | 46509 | 19.25 |
| 2 | Cart_Screen_Appear | 42338 | 17.52 |
| 3 | Payment_Screen_Successful | 33949 | 14.05 |
| 4 | Tutorial | 1010 | 0.42 |
По таблице и графику мы видим:
#Cчитаем, сколько пользователей совершали каждое из событий и долю пользователей, которые хоть раз совершали событие.
event_users = data.groupby('event_name').agg({'user_id': 'nunique'}).sort_values('user_id', ascending=False).reset_index()
event_users['%'] = round(event_users.user_id / data.user_id.nunique() * 100, 2)
event_users
| event_name | user_id | % | |
|---|---|---|---|
| 0 | Main_Screen_Appear | 7423 | 98.47 |
| 1 | Offers_Screen_Appear | 4596 | 60.97 |
| 2 | Cart_Screen_Appear | 3736 | 49.56 |
| 3 | Payment_Screen_Successful | 3540 | 46.96 |
| 4 | Tutorial | 843 | 11.18 |
По таблице мы видим:
Чтобы преположить порядок событий, посмотрим какие есть последователности у каждого из пользователей, и посчитаем частоту встречи каждой из последовательностей
#Создадим агрегированную таблицу по пользователям и событиям, и найдем для каждого события минимальное время
user_events = data.groupby(['user_id', 'event_name'], as_index=False).agg({'user_id': 'max', 'event_name': 'max', 'dt': 'min'}) \
.sort_values(['user_id', 'dt'])
#Изменим названия событий, оставив только первый символ, каждого из события
user_events.event_name = user_events.event_name.replace({'Main_Screen_Appear': 'M',
'Payment_Screen_Successful': 'P',
'Cart_Screen_Appear': 'C',
'Offers_Screen_Appear': 'O',
'Tutorial': 'T'})
#Для каждого пользователя сложим названия событий по дате
user_events = user_events.groupby('user_id', as_index=False).agg({'event_name': 'sum'})
user_events.head()
| user_id | event_name | |
|---|---|---|
| 0 | 6888746892508752 | M |
| 1 | 6909561520679493 | MCPO |
| 2 | 6922444491712477 | MCPO |
| 3 | 7435777799948366 | M |
| 4 | 7702139951469979 | MOCP |
#Теперь посчитаем частоту встречи, каждой из последовательностей событий
user_events = user_events.groupby('event_name').agg({'user_id': 'count'}).reset_index().sort_values('user_id', ascending=False)
user_events['%'] = round(user_events.user_id / user_events.user_id.sum() * 100, 2)
user_events.head(15)
| event_name | user_id | % | |
|---|---|---|---|
| 13 | M | 2703 | 35.86 |
| 22 | MOCP | 1069 | 14.18 |
| 19 | MCPO | 833 | 11.05 |
| 20 | MO | 760 | 10.08 |
| 26 | MOPC | 294 | 3.90 |
| 30 | MPCO | 282 | 3.74 |
| 68 | TMOCP | 273 | 3.62 |
| 16 | MCOP | 181 | 2.40 |
| 60 | TM | 149 | 1.98 |
| 66 | TMO | 111 | 1.47 |
| 63 | TMCOP | 92 | 1.22 |
| 45 | OMCP | 55 | 0.73 |
| 21 | MOC | 47 | 0.62 |
| 42 | OCPM | 46 | 0.61 |
| 65 | TMCPO | 44 | 0.58 |
В этой таблице представлены возможные последовательности действий пользователей и их частота встречи, по некотрым последовательностям можно сделать однозначные выводы, по другим только предположить действия пользователя:
Основной воронкой событий можно считать, такую последовательность
Однако, также стоит учитывать просмотр главного экран, так как с большой вероятностью просмотреть предложение, можно нажав на кнопку с главного экрана
#Создадим агрегируемую таблицу по событиям без туториала,
#и посчитаем сколько уникальных пользователей и всего событий произошло
funnel_events = data.query('not event_name.isin(["Tutorial"])') \
.groupby('event_name', as_index=False).agg({'user_id': ['nunique']})
funnel_events.columns = ['event_name', 'users']
funnel_events = funnel_events.sort_values('users', ascending=False).reset_index(drop=True)
#Добавим столбец с процентом, чтобы посмотреть,
#какая доля пользователей проходит на следующий шаг воронки (от числа пользователей на предыдущем)
funnel_events['%'] = round(funnel_events['users'] / funnel_events['users'].shift(1) * 100, 2)
funnel_events.loc[0, '%'] = 100
funnel_events
| event_name | users | % | |
|---|---|---|---|
| 0 | Main_Screen_Appear | 7423 | 100.00 |
| 1 | Offers_Screen_Appear | 4596 | 61.92 |
| 2 | Cart_Screen_Appear | 3736 | 81.29 |
| 3 | Payment_Screen_Successful | 3540 | 94.75 |
#Создаем график воронки
fig = go.Figure()
fig.add_trace(go.Funnel(
y = funnel_events.event_name,
x = funnel_events.users,
textinfo = "value+percent initial"))
#Добавляем название графика
fig.update_layout(title_text="График воронки событий пользователей от главного экрана до успешной оплаты",
title_font_size=18, title_x=0.55, title_y=0.85)
fig.show()
#Создаем график воронки
fig = go.Figure()
fig.add_trace(go.Funnel(
y = funnel_events.event_name.loc[1:3],
x = funnel_events.users.loc[1:3],
textinfo = "value+percent initial"))
#Добавляем название графика
fig.update_layout(title_text="График воронки событий пользователей от экрана предложения до успешной оплаты",
title_font_size=18, title_x=0.5, title_y=0.85)
fig.show()
print(f'''Процент пользователей дошедших от главного экрана до успешной оплаты \
{round(funnel_events.loc[3, 'users'] / funnel_events.loc[0, 'users'] * 100, 1)}%.''')
print(f'''Процент пользователей дошедших от экрана предложения до успешной оплаты \
{round(funnel_events.loc[3, 'users'] / funnel_events.loc[1, 'users'] * 100, 1)}%.''')
Процент пользователей дошедших от главного экрана до успешной оплаты 47.7%. Процент пользователей дошедших от экрана предложения до успешной оплаты 77.0%.
Расчет конверсии по шагам в воронке событий
#Переименуем группы для более удобного сравнения и чтения
data.group = data.group.replace({246: 'A1', 247: 'A2', 248: 'B'})
#Подсчитаем кол-во уникальных пользователей в каждой из групп
users_group = data.groupby('group').agg({'user_id': 'nunique'}).reset_index()
users_group.loc[len(users_group.index)] = ['A1_A2', sum(users_group.loc[0:1].user_id)]
users_group
| group | user_id | |
|---|---|---|
| 0 | A1 | 2484 |
| 1 | A2 | 2517 |
| 2 | B | 2537 |
| 3 | A1_A2 | 5001 |
#Проверим на наличие одинаковых пользователей в группах
print('Кол-во одинаковых пользователей в группах A1 и A2 =',
data.query('''group == "A1"''').user_id.isin(data.query('''group == "A2"''').user_id.unique()).sum())
print('Кол-во одинаковых пользователей в группах A1 и B =',
data.query('''group == "A1"''').user_id.isin(data.query('''group == "B"''').user_id.unique()).sum())
print('Кол-во одинаковых пользователей в группах A2 и B =',
data.query('''group == "A2"''').user_id.isin(data.query('''group == "B"''').user_id.unique()).sum())
Кол-во одинаковых пользователей в группах A1 и A2 = 0 Кол-во одинаковых пользователей в группах A1 и B = 0 Кол-во одинаковых пользователей в группах A2 и B = 0
В группах отсутсвуют пересечения пользователей
#Cоздадим агрегируемую таблицу, где будет указано для каждой группы с событием, кол-во уникальных пользователей.
events_group = data.query('event_name != "Tutorial"').groupby(['group', 'event_name'], as_index=False).agg({'user_id': 'nunique'})
#Также добавим в таблицую общую контрольную группу
x = data.query('event_name != "Tutorial" & group != "B"').groupby('event_name', as_index=False).agg({'user_id': 'nunique'})
x.insert(loc = 0, column = 'group', value = "A1_A2")
events_group = pd.concat([events_group, x], axis=0).reset_index(drop=True)
events_group
| group | event_name | user_id | |
|---|---|---|---|
| 0 | A1 | Cart_Screen_Appear | 1266 |
| 1 | A1 | Main_Screen_Appear | 2450 |
| 2 | A1 | Offers_Screen_Appear | 1542 |
| 3 | A1 | Payment_Screen_Successful | 1200 |
| 4 | A2 | Cart_Screen_Appear | 1239 |
| 5 | A2 | Main_Screen_Appear | 2479 |
| 6 | A2 | Offers_Screen_Appear | 1523 |
| 7 | A2 | Payment_Screen_Successful | 1158 |
| 8 | B | Cart_Screen_Appear | 1231 |
| 9 | B | Main_Screen_Appear | 2494 |
| 10 | B | Offers_Screen_Appear | 1531 |
| 11 | B | Payment_Screen_Successful | 1182 |
| 12 | A1_A2 | Cart_Screen_Appear | 2505 |
| 13 | A1_A2 | Main_Screen_Appear | 4929 |
| 14 | A1_A2 | Offers_Screen_Appear | 3065 |
| 15 | A1_A2 | Payment_Screen_Successful | 2358 |
#Создадим список событий, будем сравнивать все события, кроме туториала, так как на него приходится очень мало пользователей
event_name = events_group.sort_values('user_id', ascending=False).event_name.unique()
#Создадим функцию, которая будет проводить двухстороний z-тест сразу для всех событий
def z_test(group_1, group_2, alpha=0.05):
#Задаем критический уровень статистической значимости
alpha = alpha
print('-'*80)
for event in event_name:
#Выводим название групп и события, которые сравниваем.
print(f'Проверяем различия между выборками {group_1} и {group_2}. Cобытие - {event}.')
events = np.array([events_group.query('event_name == @event & group == @group_1').user_id,
events_group.query('event_name == @event & group == @group_2').user_id])
users = np.array([users_group.query('group == @group_1').user_id.iloc[0],
users_group.query('group == @group_2').user_id.iloc[0]])
#пропорция успехов в первой группе:
p1 = events[0]/users[0]
#пропорция успехов во второй группе:
p2 = events[1]/users[1]
#пропорция успехов в комбинированном датасете:
p_combined = (events[0] + events[1]) / (users[0] + users[1])
#разница пропорций в датасетах
difference = p1 - p2
#считаем статистику в ст.отклонениях стандартного нормального распределения
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/users[0] + 1/users[1])) # ваш код
#задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
distr = st.norm(0, 1)
#Вызовем метод cdf и вычислим и выведем p-value
p_value = (1 - distr.cdf(abs(z_value))) * 2
print('P-value равно:', round(p_value[0], 3))
#Отвергнем или не отвергним нулевую гипотезу
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
else:
print('Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными')
print('-'*80)
Гипотезы для проведения Z-теста следующие:
#Запустим двухстороний z-тест сразу для всех событий, для определения статистической значимости для А1 и A2 групп.
#Установим уровень статистической значимости 0.01
z_test('A1', 'A2', alpha=0.01)
-------------------------------------------------------------------------------- Проверяем различия между выборками A1 и A2. Cобытие - Main_Screen_Appear. P-value равно: 0.676 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными -------------------------------------------------------------------------------- Проверяем различия между выборками A1 и A2. Cобытие - Offers_Screen_Appear. P-value равно: 0.255 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными -------------------------------------------------------------------------------- Проверяем различия между выборками A1 и A2. Cобытие - Cart_Screen_Appear. P-value равно: 0.218 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными -------------------------------------------------------------------------------- Проверяем различия между выборками A1 и A2. Cобытие - Payment_Screen_Successful. P-value равно: 0.103 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными --------------------------------------------------------------------------------
Промежуточный вывод Между контрольными группами A1 и A2 нету статистической разницой, нет оснований считать доли разными. Можно сказать, что разбиение на группы работает корректно.
Гипотезы для проведения Z-теста следующие:
#Запустим двухстороний z-тест сразу для всех событий, для определения статистической значимости для А1 и B групп.
#Установим уровень статистической значимости 0.01
z_test('A1', 'B', alpha=0.01)
-------------------------------------------------------------------------------- Проверяем различия между выборками A1 и B. Cобытие - Main_Screen_Appear. P-value равно: 0.347 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными -------------------------------------------------------------------------------- Проверяем различия между выборками A1 и B. Cобытие - Offers_Screen_Appear. P-value равно: 0.208 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными -------------------------------------------------------------------------------- Проверяем различия между выборками A1 и B. Cобытие - Cart_Screen_Appear. P-value равно: 0.083 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными -------------------------------------------------------------------------------- Проверяем различия между выборками A1 и B. Cобытие - Payment_Screen_Successful. P-value равно: 0.223 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными --------------------------------------------------------------------------------
Промежуточный вывод Между группами A1 и B нету статистической разницой, нет оснований считать доли разными.
Гипотезы для проведения Z-теста следующие:
#Запустим двухстороний z-тест сразу для всех событий, для определения статистической значимости для А2 и B групп.
#Установим уровень статистической значимости 0.01
z_test('A2', 'B', alpha=0.01)
-------------------------------------------------------------------------------- Проверяем различия между выборками A2 и B. Cобытие - Main_Screen_Appear. P-value равно: 0.6 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными -------------------------------------------------------------------------------- Проверяем различия между выборками A2 и B. Cобытие - Offers_Screen_Appear. P-value равно: 0.906 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными -------------------------------------------------------------------------------- Проверяем различия между выборками A2 и B. Cобытие - Cart_Screen_Appear. P-value равно: 0.617 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными -------------------------------------------------------------------------------- Проверяем различия между выборками A2 и B. Cобытие - Payment_Screen_Successful. P-value равно: 0.678 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными --------------------------------------------------------------------------------
Промежуточный вывод Между группами A2 и B нету статистической разницой, нет оснований считать доли разными.
Гипотезы для проведения Z-теста следующие:
#Запустим двухстороний z-тест сразу для всех событий, для определения статистической значимости для А1_A2 и B групп.
#Установим уровень статистической значимости 0.01
z_test('A1_A2', 'B', alpha=0.01)
-------------------------------------------------------------------------------- Проверяем различия между выборками A1_A2 и B. Cобытие - Main_Screen_Appear. P-value равно: 0.393 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными -------------------------------------------------------------------------------- Проверяем различия между выборками A1_A2 и B. Cобытие - Offers_Screen_Appear. P-value равно: 0.429 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными -------------------------------------------------------------------------------- Проверяем различия между выборками A1_A2 и B. Cобытие - Cart_Screen_Appear. P-value равно: 0.198 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными -------------------------------------------------------------------------------- Проверяем различия между выборками A1_A2 и B. Cобытие - Payment_Screen_Successful. P-value равно: 0.645 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными --------------------------------------------------------------------------------
Промежуточный вывод Между группами A1_A2 и B нету статистической разницой, нет оснований считать доли разными.
При проверки всех гипотез, был установлен уровень стат. значимости 0.01, это означает, что в 10% случаях мы могли бы получить ложнопозитивный результат при попарном сравнение.
Всего было проверено 16 гипотез, следовательно групповая вероятность ошибки первого рода/FWER будет равна 0.148, то-есть в ~15% случаях, мы могли бы получить ложнопозитивный результат. Однако, при проверки гипотез, мы не получили не одного позитивного результата, а следовательно и ложнопозитивных результатов нету.
Таким образом, мы можем сказать, что уровень статистической значимости в 0.01 выбран верно.
Исследовательский аналих данных
Выводы по воронки событий
Расчет конверсии по шагам в воронке событий
Анализ результатов A/A/B-теста
Решение по результатам анализа A/A/B-теста